Feature sparse linalg solvers - #2841
Conversation
…oneMKL hooks
- _interface.py: add full operator algebra (.H, .T, +, *, **, neg),
_AdjointLinearOperator, _TransposedLinearOperator, _SumLinearOperator,
_ProductLinearOperator, _ScaledLinearOperator, _PowerLinearOperator,
IdentityOperator, MatrixLinearOperator, _AdjointMatrixOperator,
_CustomLinearOperator factory dispatch; extend aslinearoperator
to handle dpnp sparse and dense arrays
- _iterative.py: add _make_system (dtype validation, preconditioner
wiring, working dtype selection); add _make_fast_matvec CSR/oneMKL
SpMV hook; fix GMRES Arnoldi inner product to single oneMKL BLAS
gemv (dpnp.dot) instead of slow Python vdot loop; offload
Hessenberg lstsq to numpy.linalg.lstsq (CPU, matches CuPy);
fix SciPy host-fallback tol->rtol deprecation via _scipy_tol_kwarg;
add preconditioner support to CG; keep MINRES as SciPy-backed stub
Refs: CuPy v14.0.1 cupyx/scipy/sparse/linalg/_interface.py,
cupyx/scipy/sparse/linalg/_iterative.py"
…gmres, minres
Modeled after CuPy's cupyx_tests/scipy_tests/sparse_tests/test_linalg.py.
Covers:
- LinearOperator: shape, dtype inference, matvec/rmatvec/matmat,
subclassing, __matmul__, __call__, edge cases
- aslinearoperator: dense array, duck-type, identity passthrough,
rmatvec from dense, invalid inputs
- cg: SPD convergence, scipy reference match, x0 warm start, b_ndim=2,
callback, atol, LinearOperator path, invalid inputs,
non-convergence info check
- gmres: diag-dominant convergence, scipy reference match, restart
variants, x0, b_ndim=2, callbacks, complex systems, atol,
non-convergence info check, Hilbert-matrix stress test
- minres: SPD, symmetric-indefinite, scipy reference, shift parameter,
non-square guard, LinearOperator path, callback
- Integration: parametric (n, dtype) cross-solver tests via LinearOperator
- Import smoke tests: __all__ completeness
- Use dpnp.tests.helper: assert_dtype_allclose, generate_random_numpy_array, get_all_dtypes, get_float_complex_dtypes, has_support_aspect64 - Use dpnp.tests.third_party.cupy testing harness (with_requires, etc.) - Use numpy.testing assert_allclose / assert_array_equal / assert_raises - Use dpnp.asnumpy() instead of numpy.asarray() - Use pytest parametrize ids matching existing test conventions - Use is_scipy_available() helper from tests/helper.py - Strict class-per-solver organisation matching TestCholesky / TestDet etc.
…or dtype Two bugs fixed: 1. _init_dtype() was calling dpnp.zeros(n) which defaults to float64, so a float32 matvec would upcast and return float64, making the inferred dtype wrong. Fix: use dpnp.zeros(n, dtype=dpnp.int8) as SciPy/CuPy do — any numeric matvec will promote int8 to its own dtype. 2. _CustomLinearOperator.__init__ called _init_dtype() even when an explicit dtype was already supplied, overwriting the caller's value. Fix: _init_dtype() now short-circuits when self.dtype is already set.
…ption handling Align gemv.cpp with the conventions established in blas/gemm.cpp: Headers added: - ext/common.hpp (dpctl_td_ns, consistent with other extensions) - utils/memory_overlap.hpp (MemoryOverlap guard on x vs y) - utils/output_validation.hpp (CheckWritable + AmpleMemory on y) - utils/type_utils.hpp (validate_type_for_device<T> in impl) - <sstream> (needed for stringstream error_msg) Exception handling added in sparse_gemv_impl(): - try/catch(oneapi::mkl::exception) around all oneMKL sparse calls - try/catch(sycl::exception) around all oneMKL sparse calls - release_matrix_handle cleanup in the exception error path - throw std::runtime_error with descriptive message on catch Input validation added in sparse_gemv(): - ndim checks: x and y must be 1-D - queues_are_compatible() across all 5 USM arrays - MemoryOverlap()(x, y) aliasing guard - CheckWritable::throw_if_not_writable(y) - AmpleMemory::throw_if_not_ample(y, num_rows) - keep_args_alive() at function exit (was missing, returning empty event)
… table
Modeled after blas/gemm.cpp (2-D table: value type x index type) and
blas/gemv.cpp (dispatch vector pattern with ContigFactory + init_dispatch_table).
Changes:
- Add sparse/types_matrix.hpp with SparseGemvTypePairSupportFactory<Tv, Ti>
encoding the 4 supported combinations: {float32,float64} x {int32,int64}
- Rewrite sparse_gemv_impl() to take typeless char* pointers (matching
the blas gemv_impl signature style) — type info flows through template
params only, no runtime branching inside the impl
- Replace the 60-line if/else val_typenum/idx_typenum chain in sparse_gemv()
with a 2-D dispatch table lookup (gemv_dispatch_table[val_id][idx_id])
- Rename init_sparse_gemv_dispatch_vector -> init_sparse_gemv_dispatch_table
and implement it via init_dispatch_table<> from ext/common.hpp
- All validation guards and exception handling from prior commit are preserved
…se_gemv_dispatch_table Follows the rename made in gemv.cpp when the dispatch mechanism was changed from a 1-D vector to a 2-D table (value type x index type). All other declarations (sparse_gemv signature, parameters) are unchanged.
The oneMKL 2025-2 sparse BLAS API deprecated the old 8-argument
set_csr_data(queue, handle, nrows, ncols, index_base, row_ptr, col_ind,
values, deps) overload in favour of a new signature that takes the
sparse matrix handle as `spmat` and adds an explicit `nnz` argument:
set_csr_data(queue, spmat, nrows, ncols, nnz, index_base,
row_ptr, col_ind, values, deps)
Fixes:
- Replace old set_csr_data call with the new nnz-aware signature
- Silences the resulting -Wunused-parameter warning on `nnz` (now used)
- No functional change; all other logic is unchanged
…tring Line 477: `hasattr(A, "rmatmat\")` had a Markdown-escaped backslash leaked into the Python source, causing an unterminated string literal. Fixed to `hasattr(A, "rmatmat")`.
dpnp.ndarray blocks implicit NumPy conversion via __array__ to prevent silent dtype=object arrays. All test assertions must use .asnumpy() to materialize device arrays onto the host explicitly. Also replaces numpy.asarray(x_dp) in _rel_residual helper.
…dation order - _iterative.py: raise NotImplementedError for M != None *before* the _HOST_N_THRESHOLD SciPy fast-path in cg() and gmres(), so the contract is enforced regardless of system size (fixes test_cg_preconditioner_unsupported_raises, test_gmres_preconditioner_unsupported_raises). - _iterative.py: validate callback_type and raise NotImplementedError for 'pr_norm' *before* the _HOST_N_THRESHOLD branch in gmres(), so small-n systems also see the error (fixes test_gmres_callback_type_pr_norm_raises). - _iterative.py: pass callback_type='legacy' to scipy.sparse.linalg.gmres when delegating on the fast path to suppress SciPy DeprecationWarning. - test_scipy_sparse_linalg.py: add dtype=numpy.float64 to expected arange() calls in test_identity_operator and test_gmres_happy_breakdown so strict NumPy 2.0 dtype-equality checks pass (float64 result vs int64 expected).
… port SciPy corner cases
- Replace .asnumpy() method calls with dpnp.asnumpy() module fn (asnumpy is not an ndarray method in dpnp; it is a top-level fn) - Fix dpnp.any(x) ambiguous truth value in x0 zero-check; replace with explicit `x0 is not None` guard for r0 initialisation - Fix V_mat.T.conj() -> dpnp.conj(V_mat.T) in GMRES Arnoldi step - Guard minres beta sqrt against tiny negative floats: sqrt(abs(...)) - Unify GMRES Hessenberg h_np assignment to avoid .real stripping producing wrong dtype for complex systems - Fix float() cast on dpnp scalar norm inside GMRES inner h_j1 line
…failures) The committed code used hypot(gbar, oldb) as delta_k which is the gamma (norm) from the PREVIOUS rotation step, not the correct diagonal entry from applying the previous Givens rotation to the current column. The correct Paige-Saunders (1975) two-rotation recurrence is: oldeps = epsln delta = cs * dbar + sn * alpha # apply previous rotation gbar_k = sn * dbar - cs * alpha # residual -> new rotation input epsln = sn * beta dbar = -cs * beta gamma = hypot(gbar_k, beta) # NEW rotation eliminates beta cs = gbar_k / gamma sn = beta / gamma w_new = (v - oldeps*w - delta*w2) / gamma # three-term update This matches scipy.sparse.linalg.minres and Choi (2006) eq. 6.11. The buggy recurrence produced solutions ~1.08x away from the true solution (rel_err ~1e0) instead of the expected ~1e-13. Co-authored-by: fix-minres-recurrence
…agusetty/dpnp into feature-sparse-linalg-solvers
This reverts commit 4074478.
…agusetty/dpnp into feature-sparse-linalg-solvers
Co-authored-by: Anton <100830759+antonwolfy@users.noreply.github.com>
- _base.py: add copyright header, __all__, scipy-style issparse docstring - __init__.py: drop SparseABC from public __all__ (scipy does not expose it) - _csr.py: add copyright header - _csr.py: accept dpnp.ndarray and usm_ndarray via is_supported_array_type - _csr.py: use issparse for the csr-copy construction branch - _csr.py: validate components with check_supported_arrays_type - _csr.py: check queues via get_execution_queue + ExecutionPlacementError - _csr.py: support empty csr_matrix((M, N)) and CSR shape inference - _csr.py: allocate output with empty_like(shape=) - _csr.py: chain init event via SequentialOrderManager instead of blocking wait - _csr.py: remove silent dense fallback; raise for unsupported dtype (CuPy-style) - _csr.py: handle the returned release event in __del__ and clarify guards - _csr.py: raise NotImplementedError for unsupported scipy/cupy CSR ops - linalg/__init__.py: drop unused __future__ annotations import - doc: add scipy_sparse reference page and wire into toctree - tests: cover usm_ndarray construction/dot, empty/inferred shape, unsupported dtype
- _csr.py: pass submitted_events as depends to _sparse_gemv_init - _csr.py: pass depends + register returned event on release in __del__ - _iterative.py: allocate matvec output via empty_like(shape=) - _iterative.py: drop stale _make_fast_matvec fallback comments
Mirror SciPy's scipy/_lib/_sparse.py layout for the shared SparseABC / issparse helpers. - add dpnp/scipy/_lib package - update imports in scipy.sparse __init__ and _csr - register dpnp.scipy._lib in setup.py
- _csr.py: asarray-normalize dense/component inputs to dpnp.ndarray so internal ops (sort_indices, toarray, dot) work uniformly - _csr.py, _iterative.py: pass dpnp.get_usm_ndarray(...) to the sparse pybind bindings, accepting both dpnp.ndarray and usm_ndarray
- _interface.py: LinearOperator.dot and aslinearoperator accept usm_ndarray via is_supported_array_type (keep numpy-reject message) - _interface.py: numpydoc for LinearOperator, matvec, rmatvec, matmat, rmatmat, dot, aslinearoperator (Parameters/Returns, scipy refs) - _iterative.py: scipy-style numpydoc for cg, gmres, minres - tests: cover aslinearoperator/matvec with usm_ndarray dense input
…agusetty/dpnp into feature-sparse-linalg-solvers
…pty-CSR dot Avoid .reshape (usm_ndarray lacks it), route sparse operators through 1-D SpMV, and short-circuit nnz==0 in csr_matrix.dot.
…agusetty/dpnp into feature-sparse-linalg-solvers
|
@antonwolfy Apologies for the delay in getting back to your comments. All of them were addressed, whenever you get a chance. Thanks again! |
|
|
||
| csr_matrix((M, N), [dtype=...]) | ||
| an empty (all-zero) matrix of shape ``(M, N)``; ``dtype`` | ||
| defaults to float64. |
There was a problem hiding this comment.
It should be a default floating point data type for the device on which the input data is allocated
| shape : tuple of int | ||
| dtype : dpnp dtype | ||
| nnz : int |
There was a problem hiding this comment.
Missing description of attributes
| Sparse linear algebra interface for DPNP. | ||
|
|
||
| This module provides a subset of :mod:`scipy.sparse.linalg` | ||
| functionality on top of DPNP arrays. |
There was a problem hiding this comment.
| functionality on top of DPNP arrays. | |
| functionality on top of DPNP arrays. |
| Duplicate column indices within a row are not supported (unlike | ||
| scipy, which sums them); each column must appear at most once per | ||
| row. This matches the CSR produced by dense construction and the | ||
| solvers, which never generate duplicates. | ||
|
|
||
| Supported operations: construction, ``dot`` (matvec) via cached | ||
| oneMKL SpMV, ``toarray``, ``copy``. This is a solver-support | ||
| subset of the scipy/cupy CSR API; arithmetic, indexing, reductions, | ||
| transpose, format conversion and element-wise math are not | ||
| implemented (the most common such methods raise | ||
| ``NotImplementedError``). Convert with ``toarray()`` and use dpnp | ||
| for those. |
There was a problem hiding this comment.
It seems that has to be moved after the attributes block, otherwise rendered badly
|
|
||
| def __init__(self, dtype, shape): | ||
| if dtype is not None: | ||
| dtype = dpnp.dtype(dtype) |
There was a problem hiding this comment.
to ensure dtype is supported:
| dtype = dpnp.dtype(dtype) | |
| dtype = dpnp.empty(0, dtype=dtype).dtype |
| self._shape = (nrows, ncols) | ||
| self._has_sorted_indices = True | ||
|
|
||
| def _init_from_components(self, arrays, shape, dtype=None, copy=False): |
There was a problem hiding this comment.
dpnp might pass 7 to oneMKL -> OOB read of length-3 x for below example:
sp.csr_matrix((np.array([1.,2.]), np.array([0,7],np.int32), np.array([0,1,2],np.int32)), shape=(2,3))
# Out: <2x3 csr_matrix of dtype float64 with 2 stored elements>while scipy:
a = sp.csr_matrix((np.array([1.,2.]), np.array([0,7],np.int32), np.array([0,1,2],np.int32)), shape=(2,3))
a.check_format()
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[9], line 1
----> 1 a.check_format()
File /localdisk/work/antonvol/miniforge3/envs/dpnp_dev/lib/python3.13/site-packages/scipy/sparse/_compressed.py:212, in _cs_matrix.check_format(self, full_check)
210 if self.nnz > 0:
211 if self.indices.max() >= N:
--> 212 raise ValueError(f"indices must be < {N}")
213 if self.indices.min() < 0:
214 raise ValueError("indices must be >= 0")
ValueError: indices must be < 3| self._shape = (nrows, ncols) | ||
| self._has_sorted_indices = True | ||
|
|
||
| def _init_from_components(self, arrays, shape, dtype=None, copy=False): |
There was a problem hiding this comment.
- scipy: silent at construction, ValueError in check_format
- dpnp: row_lengths = diff = [2,-1] -> negative count to dpnp.repeat / bad offsets to oneMKL
sp.csr_matrix((np.array([1.,2.,3.]), np.array([0,1,2],np.int32), np.array([0,2,1],np.int32)), shape=(2,3))| cache->descr, {ev_opt}); | ||
| } | ||
|
|
||
| return mkl_sparse::spmv(exec_q, mkl_trans, &alpha, cache->view, |
There was a problem hiding this comment.
alpha/beta passed by address of stack, because oneMath spmv takes const void* alpha and so might be destroyed when gemv_compute_impl returns
| # No native SpMM: emulate as a column loop of 1-D SpMVs (no densify). | ||
| if issparse(self.A): | ||
| return dpnp.stack( | ||
| [self.A.dot(X[:, i]) for i in range(X.shape[1])], axis=-1 |
There was a problem hiding this comment.
self.A.dot(X[:, i]) on a C-contiguous 2-D X passes a non-unit-stride view.
sparse_gemv_compute only checks ndim/shape/dtype and reads x.get_data() contiguously → wrong results for L @ X/L.matmat(X) with ≥2 columns, no error.
| exec_q = self.data.sycl_queue | ||
| _manager = _dpu.SequentialOrderManager[exec_q] | ||
| # pylint: disable-next=protected-access | ||
| handle, val_type_id, ev = _si._sparse_gemv_init( |
There was a problem hiding this comment.
The below example raises exception here:
import dpnp as np
import dpnp.scipy.sparse as sp
b0 = np.zeros(4, dtype=np.float64)
A_empty = sp.csr_matrix((4, 4), dtype=np.float64)
sp.linalg.cg(A_empty, b0, rtol=1e-8)
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
Cell In[12], line 1
----> 1 sp.linalg.cg(A_empty, b0, rtol=1e-8)
File /localdisk/work/antonvol/code/dpnp/dpnp/scipy/sparse/linalg/_iterative.py:373, in cg(A, b, x0, rtol, tol, maxiter, M, callback, atol)
365 warnings.warn(
366 "'tol' is deprecated in favor of 'rtol' and will be removed in "
367 "a future release; use 'rtol' instead.",
368 DeprecationWarning,
369 stacklevel=2,
370 )
371 rtol = tol
--> 373 A_op, M_op, x, b, dtype = _make_system(A, M, x0, b)
374 n = b.shape[0]
376 bnrm = dpnp.linalg.norm(b)
File /localdisk/work/antonvol/code/dpnp/dpnp/scipy/sparse/linalg/_iterative.py:271, in _make_system(A, M, x0, b)
268 M_op = _FastMOp()
270 # Inject fast CSR SpMV for A if available.
--> 271 fast_mv = _make_fast_matvec(A)
272 if fast_mv is not None:
273 _orig = A_op
File /localdisk/work/antonvol/code/dpnp/dpnp/scipy/sparse/linalg/_iterative.py:178, in _make_fast_matvec(A)
176 return None
177 # pylint: disable-next=protected-access
--> 178 handle_info = A._ensure_spmv_handle()
179 if handle_info is None:
180 return None
File /localdisk/work/antonvol/code/dpnp/dpnp/scipy/sparse/_csr.py:445, in csr_matrix._ensure_spmv_handle(self)
443 _manager = _dpu.SequentialOrderManager[exec_q]
444 # pylint: disable-next=protected-access
--> 445 handle, val_type_id, ev = _si._sparse_gemv_init(
446 exec_q,
447 0, # trans=N (forward)
448 _dpnp.get_usm_ndarray(self.indptr),
449 _dpnp.get_usm_ndarray(self.indices),
450 _dpnp.get_usm_ndarray(self.data),
451 int(self._shape[0]),
452 int(self._shape[1]),
453 int(self.data.shape[0]),
454 _manager.submitted_events,
455 )
457 # set_csr_data + optimize_gemv must complete before the first
458 # compute; chain the init event through the queue's order manager
459 # so the first _sparse_gemv_compute depends on it (non-blocking).
460 _manager.add_event_pair(ev, ev)
RuntimeError: sparse_gemv_init: MKL exception in set_csr_data: oneapi::mkl::sparse::set_csr_data: invalid argument: nnz is = 0 provided with at least one of `ja`/`a` != nullptrbut passes with scipy
Adds support for
from dpnp.scipy.sparse.linalg import LinearOperator, cg, gmres, minresFixes: #2831